package v1

import (
	"context"

	"github.com/pkg/errors"
	"github.com/spf13/viper"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
	"google.golang.org/protobuf/encoding/protojson"
	"google.golang.org/protobuf/proto"
	"google.golang.org/protobuf/types/known/emptypb"

	"quipus/internal/database/model"
	"quipus/internal/utils"
	v1pb "quipus/proto/gen/api/v1"
	apb "quipus/proto/gen/assist"
	cpb "quipus/proto/gen/common"
)

func (s *APIV1Service) CreateIdentityProvider(ctx context.Context, request *v1pb.CreateIdentityProviderRequest) (*v1pb.IdentityProvider, error) {
	currentUser, err := s.GetCurrentUser(ctx)
	if err != nil {
		return nil, status.Errorf(codes.Internal, "failed to get user: %v", err)
	}
	if currentUser.Role != cpb.Role_HOST.String() && currentUser.Role != cpb.Role_ADMIN.String() && currentUser.Role != cpb.Role_TENANT_ADMIN.String() {
		return nil, status.Errorf(codes.PermissionDenied, "permission denied")
	}

	provider := convertIdentityProviderToDB(request)
	if provider == nil {
		return nil, status.Errorf(codes.InvalidArgument, "invalid identity provider")
	}
	provider.TenantID = currentUser.TenantID
	err = s.Gds.Handler.CreateIdentityProvider(ctx, provider)
	if err != nil {
		return nil, status.Errorf(codes.Internal, "failed to create identity provider, error: %+v", err)
	}
	return convertIdentityProviderFromDB(provider), nil
}

func (s *APIV1Service) ListIdentityProviders(ctx context.Context, _ *v1pb.ListIdentityProvidersRequest) (*v1pb.ListIdentityProvidersResponse, error) {
	identityProviders, err := s.Gds.Handler.ListIdentityProviders(ctx, &apb.FindIdentityProvider{})
	if err != nil {
		return nil, status.Errorf(codes.Internal, "failed to list identity providers, error: %+v", err)
	}

	response := &v1pb.ListIdentityProvidersResponse{
		IdentityProviders: []*v1pb.IdentityProvider{},
	}
	for _, identityProvider := range identityProviders {
		response.IdentityProviders = append(response.IdentityProviders, convertIdentityProviderFromDB(identityProvider))
	}
	return response, nil
}

func (s *APIV1Service) GetIdentityProvider(ctx context.Context, request *v1pb.GetIdentityProviderRequest) (*v1pb.IdentityProvider, error) {
	id, err := utils.ExtractIDFromUrn(utils.IdentityProviderUrnPrefix, request.Urn)
	if err != nil {
		return nil, status.Errorf(codes.InvalidArgument, "invalid identity provider name: %v", err)
	}

	identityProvider, err := s.Gds.Handler.GetIdentityProviderById(ctx, id)
	if err != nil {
		return nil, status.Errorf(codes.Internal, "failed to get identity provider, error: %+v", err)
	}
	if identityProvider == nil {
		return nil, status.Errorf(codes.NotFound, "identity provider not found")
	}
	return convertIdentityProviderFromDB(identityProvider), nil
}

func (s *APIV1Service) UpdateIdentityProvider(ctx context.Context, request *v1pb.UpdateIdentityProviderRequest) (*v1pb.IdentityProvider, error) {
	if request.UpdateMask == nil || len(request.UpdateMask.Paths) == 0 {
		return nil, status.Errorf(codes.InvalidArgument, "update_mask is required")
	}

	id, err := utils.ExtractIDFromUrn(utils.IdentityProviderUrnPrefix, request.IdentityProvider.Urn)
	if err != nil {
		return nil, status.Errorf(codes.InvalidArgument, "invalid identity provider name: %v", err)
	}
	update := &apb.UpdateIdentityProvider{
		Id:   id,
		Type: cpb.IdpType(cpb.IdpType_value[request.IdentityProvider.Type.String()]),
	}
	for _, field := range request.UpdateMask.Paths {
		switch field {
		case "title":
			update.Name = request.IdentityProvider.Title
		case "identifier_filter":
			update.IdentifierFilter = request.IdentityProvider.IdentifierFilter
		case "config":
			update.Config, err = convertIdentityProviderConfigToDB(request.IdentityProvider.Type, request.IdentityProvider.Config)
			if err != nil {
				return nil, status.Errorf(codes.InvalidArgument, "invalid identity provider config: %v", err)
			}
		}
	}

	identityProvider, err := s.Gds.Handler.UpdateIdentityProvider(ctx, update)
	if err != nil {
		return nil, status.Errorf(codes.Internal, "failed to update identity provider, error: %+v", err)
	}
	return convertIdentityProviderFromDB(identityProvider), nil
}

func (s *APIV1Service) DeleteIdentityProvider(ctx context.Context, request *v1pb.DeleteIdentityProviderRequest) (*emptypb.Empty, error) {
	id, err := utils.ExtractIDFromUrn(utils.IdentityProviderUrnPrefix, request.Urn)
	if err != nil {
		return nil, status.Errorf(codes.InvalidArgument, "invalid identity provider name: %v", err)
	}
	if err := s.Gds.Handler.DeleteIdentityProvider(ctx, id); err != nil {
		return nil, status.Errorf(codes.Internal, "failed to delete identity provider, error: %+v", err)
	}
	return &emptypb.Empty{}, nil
}

func convertIdentityProviderFromDB(identityProvider *model.Idp) *v1pb.IdentityProvider {
	temp := &v1pb.IdentityProvider{
		Urn:              utils.BuildUrn(utils.IdentityProviderUrnPrefix, identityProvider.ID),
		Title:            identityProvider.Name,
		IdentifierFilter: identityProvider.IdentifierFilter,
		Type:             cpb.IdpType(cpb.IdpType_value[identityProvider.Type]),
	}
	ic := &v1pb.IdentityProviderConfig{}
	var idpconfig []byte
	var e error
	key := []byte(viper.GetString("encryption-key"))
	if identityProvider.Config != "" && key != nil {
		idpconfig, e = utils.DecryptByte(identityProvider.Config, key)
		if e != nil {
			return nil
		}
	}
	switch temp.Type {
	case cpb.IdpType_OAUTH2:
		oauth := &v1pb.OAuth2Config{}
		if err := protojson.Unmarshal(idpconfig, oauth); err != nil {
			return nil
		}
		ic.Config = &v1pb.IdentityProviderConfig_Oauth2Config{
			Oauth2Config: oauth,
		}
	case cpb.IdpType_weixin:
		wx := &v1pb.WeixinConfig{}
		if err := protojson.Unmarshal(idpconfig, wx); err != nil {
			return nil
		}
		wx.SecretKey = ""
		ic.Config = &v1pb.IdentityProviderConfig_WeixinConfig{
			WeixinConfig: wx,
		}
	}

	temp.Config = ic
	return temp
}

func convertIdentityProviderToDB(identityProvider *v1pb.CreateIdentityProviderRequest) *model.Idp {
	c, e := convertIdentityProviderConfigToDB(identityProvider.Type, identityProvider.Config)
	if e != nil {
		return nil
	}
	temp := &model.Idp{
		Name:             identityProvider.Title,
		IdentifierFilter: identityProvider.IdentifierFilter,
		Type:             identityProvider.Type.String(),
		Config:           c,
	}
	return temp
}

func convertIdentityProviderConfigToDB(idpType cpb.IdpType, config *v1pb.IdentityProviderConfig) (string, error) {
	marshal := func(m proto.Message) (string, error) {
		b, err := protojson.Marshal(m)
		if err != nil {
			return "", nil
		}
		cs := string(b)
		if cs != "" {
			key := []byte(viper.GetString("encryption-key"))
			if key != nil {
				return utils.Encrypt(cs, key)
			}
		}
		return cs, nil
	}

	switch idpType {
	case cpb.IdpType_OAUTH2:
		return marshal(config.GetOauth2Config())
	case cpb.IdpType_weixin:
		return marshal(config.GetWeixinConfig())
	default:
		return "", errors.New("invalid idp type")
	}
}
